//
// Created by 常磊 on 2022/4/5.
//

import Foundation
import Alamofire

public class DioRequest {
    fileprivate let core: Request
    
    public var dataCore: Request {
        core
    }
    
    init(_ request: Request) {
        core = request
    }
    
    // MARK: - Initial State
    
    public var id: UUID {
        dataCore.id
    }
    
    public var underlyingQueue: DispatchQueue {
        dataCore.underlyingQueue
    }
    
    public var serializationQueue: DispatchQueue {
        dataCore.serializationQueue
    }
    
    public var eventMonitor: EventMonitor? {
        dataCore.eventMonitor
    }
    
    public var interceptor: RequestInterceptor? {
        dataCore.interceptor
    }
    
    public weak var delegate: DioRequestDelegate? {
        RequestAssignor(dataCore.delegate)
    }
    
    // MARK: - Mutable State
    
    public var state: Request.State {
        dataCore.state
    }
    
    public var isInitialized: Bool {
        dataCore.isInitialized
    }
    
    public var isResumed: Bool {
        dataCore.isResumed
    }
    
    public var isSuspended: Bool {
        dataCore.isSuspended
    }
    
    public var isCancelled: Bool {
        dataCore.isCancelled
    }
    
    public var isFinished: Bool {
        dataCore.isFinished
    }
    
    // MARK: Progress
    
    public typealias ProgressHandler = Request.ProgressHandler
    
    public var uploadProgress: Progress {
        dataCore.uploadProgress
    }
    
    public var downloadProgress: Progress {
        dataCore.downloadProgress
    }
    
    // MARK: Redirect Handling
    
    public var redirectHandler: RedirectHandler? {
        dataCore.redirectHandler
    }
    
    // MARK: Cached Response Handling
    
    public var cachedResponseHandler: CachedResponseHandler? {
        dataCore.cachedResponseHandler
    }
    
    // MARK: URLCredential
    
    public var credential: URLCredential? {
        dataCore.credential
    }
    
    // MARK: URLRequests
    
    public var requests: [URLRequest] {
        dataCore.requests
    }
    
    public var firstRequest: URLRequest? {
        dataCore.firstRequest
    }
    
    public var lastRequest: URLRequest? {
        dataCore.lastRequest
    }
    
    public var request: URLRequest? {
        dataCore.request
    }
    
    public var performedRequests: [URLRequest] {
        dataCore.performedRequests
    }
    
    // MARK: HTTPURLResponse
    
    public var response: HTTPURLResponse? {
        dataCore.response
    }
    
    // MARK: Tasks
    
    public var tasks: [URLSessionTask] {
        dataCore.tasks
    }
    
    public var firstTask: URLSessionTask? {
        dataCore.firstTask
    }
    
    public var lastTask: URLSessionTask? {
        dataCore.lastTask
    }
    
    public var task: URLSessionTask? {
        dataCore.task
    }
    
    // MARK: Metrics
    
    public var allMetrics: [URLSessionTaskMetrics] {
        dataCore.allMetrics
    }
    
    public var firstMetrics: URLSessionTaskMetrics? {
        dataCore.firstMetrics
    }
    
    public var lastMetrics: URLSessionTaskMetrics? {
        dataCore.lastMetrics
    }
    
    public var metrics: URLSessionTaskMetrics? {
        dataCore.metrics
    }
    
    // MARK: Retry Count
    
    public var retryCount: Int {
        dataCore.retryCount
    }
    
    // MARK: Error
    
    public var error: DioError? {
        dataCore.error
    }
    
    // MARK: - Public API
    
    // These APIs are callable from any queue.
    
    // MARK: State
    
    @discardableResult
    public func cancel() -> Self {
        dataCore.cancel()
        return self
    }
    
    @discardableResult
    public func suspend() -> Self {
        dataCore.suspend()
        return self
    }
    
    @discardableResult
    public func resume() -> Self {
        dataCore.resume()
        return self
    }
    
    // MARK: - Closure API
    
    @discardableResult
    public func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self {
        let credential = URLCredential(user: username, password: password, persistence: persistence)
        
        return authenticate(with: credential)
    }
    
    @discardableResult
    public func authenticate(with credential: URLCredential) -> Self {
        dataCore.authenticate(with: credential)
        return self
    }
    
    @discardableResult
    public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self {
        dataCore.downloadProgress(queue: queue, closure: closure)
        return self
    }
    
    @discardableResult
    public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self {
        dataCore.uploadProgress(queue: queue, closure: closure)
        return self
    }
    
    // MARK: Redirects
    
    @discardableResult
    public func redirect(using handler: RedirectHandler) -> Self {
        dataCore.redirect(using: handler)
        return self
    }
    
    // MARK: Cached Responses
    
    @discardableResult
    public func cacheResponse(using handler: CachedResponseHandler) -> Self {
        dataCore.cacheResponse(using: handler)
        return self
    }
    
    // MARK: - Lifetime APIs
    
    @discardableResult
    public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self {
        dataCore.cURLDescription(on: queue, calling: handler)
        return self
    }
    
    @discardableResult
    public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self {
        dataCore.cURLDescription(calling: handler)
        return self
    }
    
    @discardableResult
    public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self {
        dataCore.onURLRequestCreation(on: queue, perform: handler)
        return self
    }
    
    @discardableResult
    public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self {
        dataCore.onURLSessionTaskCreation(on: queue, perform: handler)
        return self
    }
}

// MARK: - Protocol Conformances

extension DioRequest: Equatable {
    public static func ==(lhs: DioRequest, rhs: DioRequest) -> Bool {
        lhs.id == rhs.id
    }
}

extension DioRequest: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

extension DioRequest: CustomStringConvertible {
    public var description: String {
        guard let request = performedRequests.last ?? lastRequest,
              let url = request.url,
              let method = request.httpMethod else {
            return "No request created yet."
        }
        
        let requestDescription = "\(method) \(url.absoluteString)"
        
        return response.map {
            "\(requestDescription) (\($0.statusCode))"
        } ?? requestDescription
    }
}

extension DioRequest {
    public func cURLDescription() -> String {
        guard let request = lastRequest,
              let url = request.url,
              let host = url.host,
              let method = request.httpMethod else {
            return "$ curl command could not be created"
        }
        
        var components = ["$ curl -v"]
        
        components.append("-X \(method)")
        
        if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage {
            let protectionSpace = URLProtectionSpace(
                host: host,
                port: url.port ?? 0,
                protocol: url.scheme,
                realm: host,
                authenticationMethod: NSURLAuthenticationMethodHTTPBasic
            )
            
            if let credentials = credentialStorage.credentials(for: protectionSpace)?.values {
                for credential in credentials {
                    guard let user = credential.user, let password = credential.password else {
                        continue
                    }
                    components.append("-u \(user):\(password)")
                }
            } else {
                if let credential = credential, let user = credential.user, let password = credential.password {
                    components.append("-u \(user):\(password)")
                }
            }
        }
        
        if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies {
            if
                let cookieStorage = configuration.httpCookieStorage,
                let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty {
                let allCookies = cookies.map {
                    "\($0.name)=\($0.value)"
                }.joined(separator: ";")
                
                components.append("-b \"\(allCookies)\"")
            }
        }
        
        var headers = HTTPHeaders()
        
        if let sessionHeaders = delegate?.sessionConfiguration.headers {
            for header in sessionHeaders where header.name != "Cookie" {
                headers[header.name] = header.value
            }
        }
        
        for header in request.headers where header.name != "Cookie" {
            headers[header.name] = header.value
        }
        
        for header in headers {
            let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"")
            components.append("-H \"\(header.name): \(escapedValue)\"")
        }
        
        if let httpBodyData = request.httpBody {
            let httpBody = String(decoding: httpBodyData, as: UTF8.self)
            var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"")
            escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"")
            
            components.append("-d \"\(escapedBody)\"")
        }
        
        components.append("\"\(url.absoluteString)\"")
        
        return components.joined(separator: " \\\n\t")
    }
}

// MARK: - Subclasses

// MARK: - DataRequest

public class DioDataRequest: DioRequest {
    public override var dataCore: DataRequest {
        super.core as! DataRequest
    }
    
    public var convertible: URLRequestConvertible {
        dataCore.convertible
    }
    
    public var data: Data? {
        dataCore.data
    }
    
    init(_ request: DataRequest) {
        super.init(request)
    }
    
    @discardableResult
    public func validate(_ validation: @escaping Validation) -> Self {
        dataCore.validate(validation)
        return self
    }
}

// MARK: - DataStreamRequest

public final class DioDataStreamRequest: DioRequest {
    public typealias Handler<Success, Failure: Error> = DataStreamRequest.Handler<Success, Failure>
    public typealias Stream<Success, Failure: Error> = DataStreamRequest.Stream<Success, Failure>
    
    public override var dataCore: DataStreamRequest {
        super.core as! DataStreamRequest
    }
    
    public var convertible: URLRequestConvertible {
        dataCore.convertible
    }
    
    public var automaticallyCancelOnStreamError: Bool {
        dataCore.automaticallyCancelOnStreamError
    }
    
    init(_ request: DataStreamRequest) {
        super.init(request)
    }
    
    @discardableResult
    public func validate(_ validation: @escaping Validation) -> Self {
        dataCore.validate(validation)
        return self
    }
    
#if !(os(Linux) || os(Windows))
    public func asInputStream(bufferSize: Int = 1024) -> InputStream? {
        dataCore.asInputStream(bufferSize: bufferSize)
    }
#endif
}

// MARK: - DownloadRequest

public class DioDownloadRequest: DioRequest {
    public typealias Destination = DownloadRequest.Destination
    public typealias Downloadable = DownloadRequest.Downloadable
    
    public override var dataCore: DownloadRequest {
        super.core as! DownloadRequest
    }
    
    // MARK: Destination
    
    public class func suggestedDownloadDestination(
        for directory: FileManager.SearchPathDirectory = .documentDirectory,
        in domain: FileManager.SearchPathDomainMask = .userDomainMask,
        options: DownloadRequest.Options = []
    ) -> Destination {
        DownloadRequest.suggestedDownloadDestination(
            for: directory,
            in: domain,
            options: options
        )
    }
    
    // MARK: Mutable State
    
    public var resumeData: Data? {
        dataCore.resumeData
    }
    
    public var fileURL: URL? {
        dataCore.fileURL
    }
    
    // MARK: Initial State
    
    public var downloadable: Downloadable {
        dataCore.downloadable
    }
    
    init(_ request: DownloadRequest) {
        super.init(request)
    }
    
    public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask {
        dataCore.task(forResumeData: data, using: session)
    }
    
    @discardableResult
    override public func cancel() -> Self {
        cancel(producingResumeData: false)
    }
    
    @discardableResult
    public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self {
        dataCore.cancel(producingResumeData: shouldProduceResumeData)
        return self
    }
    
    @discardableResult
    public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self {
        dataCore.cancel(byProducingResumeData: completionHandler)
        return self
    }
    
    @discardableResult
    public func validate(_ validation: @escaping Validation) -> Self {
        dataCore.validate(validation)
        return self
    }
}

// MARK: - UploadRequest

public class DioUploadRequest: DioDataRequest {
    public typealias Uploadable = UploadRequest.Uploadable
    
    public override var dataCore: UploadRequest {
        super.core as! UploadRequest
    }
    
    // MARK: Initial State
    
    public var upload: UploadableConvertible {
        dataCore.upload
    }
    
    public var fileManager: FileManager {
        dataCore.fileManager
    }
    
    // MARK: Mutable State
    
    public var uploadable: Uploadable? {
        dataCore.uploadable
    }
    
    init(_ request: UploadRequest) {
        super.init(request)
    }
    
    public func cleanup() {
        dataCore.cleanup()
    }
}

public protocol DioRequestDelegate: AnyObject {
    var sessionConfiguration: URLSessionConfiguration { get }
    
    var startImmediately: Bool { get }
    
    func cleanup(after request: DioRequest)
    
    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
    func retryResult(for request: DioRequest, dueTo error: DioError) async -> RetryResult
    
    func retryRequest(_ request: DioRequest, withDelay timeDelay: TimeInterval?)
}

public class RequestAssignor: DioRequestDelegate, RequestDelegate {
    fileprivate let core: RequestDelegate
    
    init?(_ delegate: RequestDelegate?) {
        guard let delegate = delegate else {
            return nil
        }
        core = delegate
    }
    
    public var sessionConfiguration: URLSessionConfiguration {
        core.sessionConfiguration
    }
    
    public var startImmediately: Bool {
        core.startImmediately
    }
    
    public func cleanup(after request: DioRequest) {
        cleanup(after: request.core)
    }
    
    public func retryRequest(_ request: DioRequest, withDelay timeDelay: TimeInterval?) {
        retryRequest(request.core, withDelay: timeDelay)
    }
    
    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
    public func retryResult(for request: DioRequest, dueTo error: DioError) async -> RetryResult {
        await withTaskCancellationHandler {
            await withCheckedContinuation { (continuation: CheckedContinuation<RetryResult, Never>) in
                retryResult(for: request.core, dueTo: error) { result in
                    continuation.resume(returning: result)
                }
            }
        } onCancel: {
            request.cancel()
        }
    }
    
    public func cleanup(after request: Request) {
        core.cleanup(after: request)
    }
    
    public func retryResult(for request: Request, dueTo error: DioError, completion: @escaping (RetryResult) -> Void) {
        core.retryResult(for: request, dueTo: error, completion: completion)
    }
    
    public func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) {
        core.retryRequest(request, withDelay: timeDelay)
    }
}
